package com.gitee.wsl.model.process.plugins

import com.gitee.wsl.model.OutgoingContent
import com.gitee.wsl.model.process.ProcessModel
import com.gitee.wsl.model.process.ProcessModelPlugin
import com.gitee.wsl.model.process.call.ProcessClientCall
import com.gitee.wsl.model.process.request.ProcessRequestBuilder
import com.gitee.wsl.model.process.request.RequestPipeline
import com.gitee.wsl.model.process.request.setBody
import com.gitee.wsl.prop.AttributeKey
import kotlinx.coroutines.cancel

/**
 * HttpSend pipeline interceptor function
 */
 typealias ProcessSendInterceptor = suspend Sender.(ProcessRequestBuilder) -> ProcessClientCall

/**
 * This interface represents a request send pipeline interceptor chain
 */
 interface Sender {
    /**
     * Execute send pipeline. It could start pipeline execution or replace the call
     */
     suspend fun execute(requestBuilder: ProcessRequestBuilder): ProcessClientCall
}

/**
 * This is an internal plugin that is always installed.
 */
class ProcessSend private constructor(
    private val maxSendCount: Int = 20
) {
    
     class Config {
        /**
         * Maximum number of requests that can be sent during a call
         */
         var maxSendCount: Int = 20
    }

    private val interceptors: MutableList<ProcessSendInterceptor> = mutableListOf()

    /**
     * Install send pipeline starter interceptor
     */
     fun intercept(block: ProcessSendInterceptor) {
        interceptors += block
    }

    /**
     * A plugin's installation object
     */
     companion object Plugin : ProcessModelPlugin<Config,ProcessSend> {

        override val key: AttributeKey<ProcessSend> = AttributeKey("HttpSend")

        override fun install(plugin: ProcessSend, scope: ProcessModel) {
            // default send scenario
            scope.requestPipeline.intercept(RequestPipeline.Send) { content ->
                check(content is OutgoingContent) {
                    """
                     |Fail to prepare request body for sending. 
                     |The body type is: ${content::class}, with Content-Type: ${context}.
                     |
                     |If you expect serialized body, please check that you have installed the corresponding plugin(like `ContentNegotiation`) and set `Content-Type` header."""
                        .trimMargin()
                }
                context.setBody(content)

                val realSender: Sender = DefaultSender(plugin.maxSendCount, scope)
                var interceptedSender = realSender
                for (interceptor in plugin.interceptors.reversed()) {
                    interceptedSender = InterceptedSender(interceptor, interceptedSender)
                }
                val call = interceptedSender.execute(context)
                proceedWith(call)
            }
        }


        override fun prepare(block: Config.() -> Unit): ProcessSend {
            val config = Config().apply(block)
            return ProcessSend(config.maxSendCount)
        }

    }

    private class InterceptedSender(
        private val interceptor: ProcessSendInterceptor,
        private val nextSender: Sender
    ) : Sender {

        override suspend fun execute(requestBuilder: ProcessRequestBuilder): ProcessClientCall {
            return interceptor.invoke(nextSender, requestBuilder)
        }
    }

    private class DefaultSender(
        private val maxSendCount: Int,
        private val client: ProcessModel
    ) : Sender {
        private var sentCount: Int = 0
        private var currentCall: ProcessClientCall? = null

        override suspend fun execute(requestBuilder: ProcessRequestBuilder): ProcessClientCall {
            currentCall?.cancel()

            if (sentCount >= maxSendCount) {
                throw SendCountExceedException(
                    "Max send count $maxSendCount exceeded. Consider increasing the property " +
                            "maxSendCount if more is required."
                )
            }

            sentCount++
            val sendResult = client.sendPipeline.execute(
                requestBuilder,
                requestBuilder.body
            )

            val call = sendResult as? ProcessClientCall
                ?: error("Failed to execute send pipeline. Expected [HttpClientCall], but received $sendResult")

            currentCall = call
            return call
        }
    }
}

/**
 * Thrown when too many actual requests were sent during a client call.
 * It could be caused by infinite or too long redirect sequence.
 * Maximum number of requests is limited by [ProcessSend.maxSendCount]
 */
 class SendCountExceedException(message: String) : IllegalStateException(message)
